#ifndef _IMPLEMENTATION_HPP
#define _IMPLEMENTATION_HPP

#include "interface.hpp"
#include "data.hpp"

#include <CL/cl.hpp>

#include <string>
#include <iostream>
#include <limits>


static inline void checkErr(cl_int err, const char * name){
    if (err != CL_SUCCESS) {
            std::cerr << "ERROR: " << name << " (" << err << ")" << std::endl;
            exit(EXIT_FAILURE);
    }
}

/*
 *  * Final implementation of the tested OCL program.
 *   */
class OCLProgram_Implementation : public OCLProgram
{
private:
    cl::Buffer * mBuffers;
    cl::CommandQueue * mCommandQueue;
    std::map<std::string, cl::Kernel> * mCompiledKernels;


    cl::Kernel & getKernel(const std::string & str){
        return (*mCompiledKernels)[str];
    }

    enum {
        IN = 0,
        TMP = 1,
        OUT = 2,
        COCKOO = 3,
        IN2 = 4
    };

    cl::Buffer & getBuffer( int type ){
        return (mBuffers[type]);
    }
    cl::CommandQueue & getCommandQueue(){
        return *mCommandQueue;
    }

    inline void dbgMsg( bool verbose,Stopwatch * stopwatch, const char * str){
        if (verbose){
            stopwatch->stop();
            std::cout << str << stopwatch->getMiliseconds() << " ms" << std::endl;
            stopwatch->start();
        }
    }


    void prepareInput(){

	size_t size = mDataIn.set1.size() + mDataIn.set2.size();
        size_t bytes_size = size * sizeof(unsigned);

        cl_int err = getCommandQueue().enqueueWriteBuffer(getBuffer(IN),CL_FALSE, 0, bytes_size/2, (const void *) &mDataIn.set1.front() );
        checkErr(err, "CommandQueue::enequeueWriteBuffer");

        err = getCommandQueue().enqueueWriteBuffer(getBuffer(IN2),CL_FALSE, 0, bytes_size/2, (const void *) &mDataIn.set2.front() );
        checkErr(err, "CommandQueue::enequeueWriteBuffer");

        err = getCommandQueue().finish();
        checkErr(err, "CommandQueue::enequeueWriteBuffer::finish");

        zeroBuffer(COCKOO, size * 2);

    }



   void zeroBuffer(int type, size_t length){
	cl_int err;
	length = (length + 3) / 4;
        cl::Kernel & zero = getKernel("zero");
	err = zero.setArg( 0, getBuffer(type) );
	checkErr(err,"zero.setArg");
	err = getCommandQueue().enqueueNDRangeKernel( zero,cl::NullRange,cl::NDRange(length),cl::NullRange );
        checkErr(err, "zefoBuffer::enqueue");
        err = getCommandQueue().finish();
        checkErr(err, "zefoBuffer::finish");

    }

   void cockooHash(){

       size_t size = mDataIn.set1.size();
       cl_int err;

       cl::Kernel & hash = getKernel("cockooHash");

       err = hash.setArg( 0, getBuffer(IN) );
       checkErr(err,"cockooHash.setArg0");

       err = hash.setArg(1,getBuffer(COCKOO));
       checkErr(err,"cockooHash.setArg1");

       cl_uint s = 2 * size;
       err = hash.setArg( 2, sizeof(s), (void*) &s );
       checkErr(err,"cockooHash.setArg2");

       err = getCommandQueue().enqueueNDRangeKernel( hash,cl::NullRange,cl::NDRange(size),cl::NullRange );
       checkErr(err, "cockooHash::enqueue");
       err = getCommandQueue().finish();
       checkErr(err, "cockooHash::finish");

   }

   void cockooFind(){
       size_t size = mDataIn.set1.size();
       cl_int err;

       cl_uint s = 2 * size;

       cl::Kernel & cockooFind = getKernel("cockooFind");
       err = cockooFind.setArg( 0, getBuffer(IN2) );
       checkErr(err,"cockooFind::setArgument:0");
       err = cockooFind.setArg( 1, getBuffer(OUT) );
       checkErr(err,"cockooFind::setArgument:1");
       err = cockooFind.setArg( 2, getBuffer(COCKOO) );
       checkErr(err,"cockooFind::setArgument:2");
       err = cockooFind.setArg( 3, 1 * sizeof(cl_uint), (void *) NULL );    //local args
       checkErr(err,"cockooFind::setArgument:3");
       err = cockooFind.setArg( 4, sizeof(s), (void*) &s );
       checkErr(err,"cockooFind::setArgument:4");
       err = cockooFind.setArg( 5, getBuffer(TMP) );
       checkErr(err,"cockooFind::setArgument:5");


       err = getCommandQueue().enqueueNDRangeKernel( cockooFind,cl::NullRange,cl::NDRange(size), cl::NDRange(1024) );
       checkErr(err, "cockooFind::enqueue");
       err = getCommandQueue().finish();
       checkErr(err, "cockooFind::finish");

//       unsigned nGroups = size / 1024;
//       cl_uint dbg_ptr[nGroups + 3];
//       err = getCommandQueue().enqueueReadBuffer(getBuffer(TMP),CL_TRUE,0,sizeof(cl_uint) * (nGroups + 3), (void *)dbg_ptr);

//       printf("%d groups, %d threads, count %d\n", dbg_ptr[nGroups + 0],dbg_ptr[nGroups + 1], dbg_ptr[ nGroups + 2] );

//       unsigned resSize = 0;
//       for( size_t k = 0; k < nGroups; ++k){
//           resSize += dbg_ptr[k];
//       }
//       printf("%d groups, %d res_size\n", nGroups, resSize);
   }

   void prefixSum(){
       cl_int err;
       cl_uint group_size = 1024;
       cl_uint work_size = mDataIn.set1.size();
       cl_uint number_groups = work_size / group_size;

       cl::Kernel & prefSum = getKernel("prefixSum");

       err = prefSum.setArg(0,getBuffer(TMP));
       checkErr(err, "prefixSum::setArg:0");
       err = prefSum.setArg(1,getBuffer(COCKOO));
       checkErr(err, "prefixSum::setArg:1");
       err = prefSum.setArg(2,getBuffer(IN2));
       checkErr(err, "prefixSum::setArg:2");
       err = prefSum.setArg( 3, group_size * 2 * sizeof(cl_uint), (void *) NULL );    //local args
       checkErr(err, "prefixSum::setArg:3");

       err = getCommandQueue().enqueueNDRangeKernel( prefSum,cl::NullRange,cl::NDRange(work_size/2),cl::NDRange(group_size/2) );
       checkErr(err, "prefixSum::ndrange");
       err = getCommandQueue().finish();
       checkErr(err, "prefixSum::finish");

       ///////////////////////////////////////

       cl::Kernel & prefSumC = getKernel("prefixSumC");
       err = prefSumC.setArg(0,getBuffer(IN2));
       checkErr(err, "prefixSumC::setArg:0");
       err = prefSumC.setArg(1,getBuffer(IN2));
       checkErr(err, "prefixSumC::setArg:1");
       cl_uint sz = number_groups / group_size;
       err = prefSumC.setArg(2,sizeof(cl_uint),&sz);
       checkErr(err, "prefixSumC::setArg:2");

       err = getCommandQueue().enqueueNDRangeKernel( prefSumC,cl::NullRange,cl::NDRange(1),cl::NDRange(1) );
       checkErr(err, "prefixSumC::ndrange");
       err = getCommandQueue().finish();
       checkErr(err, "prefixSumC::finish");

       ///////////////////////////////////////

       cl::Kernel & prefSumB = getKernel("prefixSumB");
       err = prefSumB.setArg(0,getBuffer(COCKOO));
       checkErr(err, "prefixSumB::setArg:0");
       err = prefSumB.setArg(1,getBuffer(TMP));
       checkErr(err, "prefixSumB::setArg:1");
       err = prefSumB.setArg(2,getBuffer(IN2));
       checkErr(err, "prefixSumB::setArg:2");
       err = prefSumB.setArg( 3, group_size * 2 * sizeof(cl_uint), (void *) NULL );    //local args
       checkErr(err, "prefixSumB::setArg:3");

       err = getCommandQueue().enqueueNDRangeKernel( prefSumB,cl::NullRange,cl::NDRange(work_size/2),cl::NDRange(group_size/2) );
       checkErr(err, "prefixSumB::ndrange");
       err = getCommandQueue().finish();
       checkErr(err, "prefixSumB::finish");
   }

   void runCopyByOffset(){
       cl_int err;
       cl::Kernel & copyByOffset = getKernel("copyByOffset");
       err = copyByOffset.setArg( 0, getBuffer(OUT) );
       checkErr(err, "copyByOffset::setArg:0");
       err = copyByOffset.setArg( 1, getBuffer(COCKOO) );
       checkErr(err, "copyByOffset::setArg:1");
       err = copyByOffset.setArg( 2, getBuffer(TMP) );
       checkErr(err, "copyByOffset::setArg:2");

       err = getCommandQueue().enqueueNDRangeKernel( copyByOffset,cl::NullRange,cl::NDRange(this->mDataIn.set1.size()), cl::NDRange(1024) );
       checkErr(err, "copyByOffset::enqueue");
       err = getCommandQueue().finish();
       checkErr(err, "copyByOffset::finish");

       cl_uint resSize;
       err = getCommandQueue().enqueueReadBuffer(getBuffer(OUT),CL_TRUE,0,sizeof(cl_uint) * 1, (void *)&resSize);
       checkErr(err, "copyByOffset::readResSize");
//     std::cout << "result size " << resSize << std::endl;


       err = getCommandQueue().enqueueReadBuffer(getBuffer(COCKOO),CL_TRUE,0,sizeof(cl_uint) * resSize, (void *)&mDataOut.res.front());
       checkErr(err, "copyByOffset::readRes");

       mDataOut.res.resize(resSize,0);


   }

    void computeSerial(){
	std::vector<unsigned> s1 = mDataIn.set1;
	std::vector<unsigned> s2 = mDataIn.set2;
	std::vector<unsigned> result;
	result.reserve(std::min(s1.size(),s2.size()) );
	Stopwatch stopwatch(true);
	std::sort(s1.begin(),s1.end());
	std::sort(s2.begin(),s2.end());
	mergeJoin(s1,s2,result);
	stopwatch.stop();
	std::cout << "serial took: " << stopwatch.getMiliseconds() << " ms" << std::endl;
        std::cout << "serial. result.size(): " << result.size() << std::endl;
    }

    // loc memory size in bytes 49152
    void info(const std::vector<cl::Device> &devices){
        std::string off = "    ";
        for( size_t i = 0; i < devices.size(); ++i ){
            const cl::Device & device = devices[i];
            std::cout << "device: " << i << std::endl;
            cl_ulong inf;
            device.getInfo(CL_DEVICE_LOCAL_MEM_SIZE, &inf);
            std::cout << off << "memory " << inf << std::endl;
            cl_uint iInf;
            device.getInfo(CL_DEVICE_MAX_COMPUTE_UNITS, &iInf);
            std::cout << off << "compute units " << iInf << std::endl;
            size_t t;
            device.getInfo(CL_DEVICE_MAX_WORK_GROUP_SIZE, &t);
            std::cout << off << "max work group size " << t << std::endl;
        }
    }

public:
	OCLProgram_Implementation(const DataIn &dataIn, DataOut &dataOut) : OCLProgram(dataIn, dataOut)
        {
                /* This is the best place to initialize mKernels member field.
 *                  i.e. mKernels["kernelName"] = Source("kernelFile.cl", true);
 *                                   */
		  mKernels["zero"] = Source("zero.cl",true);
                  mKernels["cockooHash"] = Source("cockooHash.cl",true);
                  mKernels["cockooFind"] = Source("cockooFind.cl",true);
                  mKernels["prefixSum"] = Source("prefixSum.cl",true);
                  mKernels["prefixSumB"] = Source("prefixSumB.cl",true);
                  mKernels["prefixSumC"] = Source("prefixSumC.cl",true);
                  mKernels["copyByOffset"] = Source("copyByOffset.cl",true);

                  mDataOut.res.assign(1024 * 1024 * 16,0);

	}


	virtual int run(const cl::Context &context, const std::vector<cl::Device> &devices,
		std::map<std::string, cl::Kernel> &kernels, bool verbose)
	{
		/*
 * 		 * Implementation of the main algorithm.
 * 		 		 * Note that you have mDataIn and mDataOut member fields at your disposal.
 *


 * 		 		                  */

           // info(devices);

            cl_int err;

            Stopwatch * stopwatch = NULL;
            if (verbose){
                stopwatch = new Stopwatch(true);
            }


            mCompiledKernels = &kernels;

            size_t size = (mDataIn.set1.size() + mDataIn.set2.size()) * sizeof(unsigned);


            cl::Buffer bufs []= {
                cl::Buffer(context, CL_MEM_READ_WRITE, size, NULL, &err ),  /*in*/
                cl::Buffer(context, CL_MEM_READ_WRITE, size, NULL, &err ),  /*tmp*/
                cl::Buffer(context, CL_MEM_READ_WRITE, size, NULL, &err ),   /*out*/
                cl::Buffer(context, CL_MEM_READ_WRITE, 2 * size, NULL, &err ),   /*COCKOO*/
                cl::Buffer(context, CL_MEM_READ_WRITE, size, NULL, &err ),  /*in2*/
            };
            checkErr(err, "cl::Buffer");
            mBuffers = bufs;

            dbgMsg(verbose, stopwatch,"Creating buffers 1: ");


            cl::CommandQueue commandQueue(context, devices[0], 0, &err);
            checkErr(err, "CommandQueue");
            mCommandQueue = &commandQueue;


            prepareInput();
            dbgMsg(verbose, stopwatch,"Filling buffers: ");

            cockooHash();
            dbgMsg(verbose, stopwatch,"Cockoo hash: ");

            cockooFind();
            dbgMsg(verbose, stopwatch,"Cockoo find: ");

            prefixSum();
            dbgMsg(verbose, stopwatch,"Prefix sum: ");

            runCopyByOffset();
            dbgMsg(verbose, stopwatch,"runCopyByOffset: ");

            if (verbose){
                stopwatch->stop();
                delete stopwatch;
            }

            return 0;	// Success
	}
};





















#endif


